iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
Modern Web

重新認識Vue 走過路過不要錯過系列 第 8

Vue 走過路過不要錯過 Day08 - 改完資料,DOM 為什麼還是舊的?認識 nextTick

  • 分享至 

  • xImage
  •  

前言

在 Day03 我們用 Proxy 做了一個迷你 reactive,只要資料一改,畫面就馬上跟著更新。

但當時我有留一個伏筆:真正的 Vue 並不是這樣做的。

今天就來把這個伏筆收回來,順便認識一個很常用、但很多人不太懂的 API:nextTick


先看一個奇怪的現象

<div id="app">
  <p ref="msg">{{ count }}</p>
  <button @click="add">+1</button>
</div>

<script type="module">
  import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

  createApp({
    setup() {
      const count = ref(0)
      const msg = ref(null)

      function add() {
        count.value++
        console.log('資料:', count.value)
        console.log('畫面:', msg.value.textContent)
      }

      return { count, msg, add }
    }
  }).mount('#app')
</script>

點一下按鈕,console 會印出:

資料: 1
畫面: 0

資料已經變成 1 了,但畫面上讀到的還是 0。

這不是 bug,而是 Vue 故意這樣設計的。


為什麼 Vue 不馬上更新畫面?

用點餐來比喻。

你在餐廳點了一碗麵、一杯紅茶、一盤小菜,服務生不會每聽到一道就跑一次廚房,而是全部寫在點單上,等你點完再一次送過去。

Vue 更新畫面也是一樣的邏輯:

  • 你改資料 → 就像在點菜
  • Vue 先把「要更新畫面」這件事記下來
  • 等這段程式碼全部跑完,再一次更新畫面

這樣做的好處是,就算你連續改了 100 次資料:

for (let i = 0; i < 100; i++) {
  count.value++
}

畫面也只會更新 1 次,而不是 100 次。操作 DOM 是很耗效能的事情,能少做就少做。

這個機制叫做批次更新


回頭看 Day03

Day03 的迷你 reactive 大概長這樣:

set(target, key, value) {
  target[key] = value
  effect() // 一改就馬上執行
  return true
}

這就像服務生每聽到一道菜就跑一次廚房。

真正的 Vue 多做了一步:不馬上執行,而是先放進一個待辦清單,晚一點再統一執行。這就是 Day03 留下的伏筆。


「晚一點」是多晚?

Vue 會把更新畫面的工作,放進 Promise.then() 裡面執行。

在 JavaScript 裡,Promise.then() 的內容會在目前的同步程式碼全部跑完之後才執行。

console.log('A')
Promise.resolve().then(() => console.log('B'))
console.log('C')

// 印出順序:A → C → B

所以 Vue 的更新流程是:

  1. 你的程式碼執行,資料被修改(同步)
  2. 你的程式碼跑完
  3. Vue 在 Promise.then() 裡一次更新畫面

這也解釋了一開始的現象:console.log 是在第 1 步執行的,那時畫面還沒更新。


nextTick 登場

如果我們改完資料,就是想拿到更新後的畫面,該怎麼辦?

答案就是 nextTick,它的意思是:等畫面更新完,再繼續往下執行。

import { ref, nextTick } from 'vue'

async function add() {
  count.value++
  await nextTick()
  console.log('畫面:', msg.value.textContent) // 1
}

nextTick 本身也是一個 Promise,而且它會排在 Vue 更新畫面的那個 Promise 後面,所以等到它的時候,畫面一定已經更新好了。


實際會用到的情境

情境一:按下編輯,輸入框自動 focus

<button @click="startEdit">編輯</button>
<input v-if="isEditing" ref="inputRef" />
const isEditing = ref(false)
const inputRef = ref(null)

async function startEdit() {
  isEditing.value = true   // 讓輸入框出現
  await nextTick()         // 等畫面更新
  inputRef.value.focus()   // 輸入框已經在畫面上了
}

如果沒有 nextTickisEditing 剛改成 true 時,輸入框還沒被建立,inputRef.valuenull,就會報錯。

情境二:新增訊息後,自動捲到最底

async function addMessage(text) {
  messages.value.push(text)
  await nextTick()
  listRef.value.scrollTop = listRef.value.scrollHeight
}

新訊息要先出現在畫面上,高度才會變,這時捲動才會捲到正確的位置。


什麼時候需要 nextTick?

記一句話就好:

改完資料後,下一步要操作畫面上的元素,就加 await nextTick()

例如 focus、捲動、量元素的寬高。

如果只是讀資料本身,就不需要,因為資料是立刻改好的,只有畫面是晚一點才更新。


小結

  • Vue 改資料後,不會馬上更新畫面,而是等程式碼跑完再一次更新(批次更新)
  • 這樣可以避免重複操作 DOM,效能比較好
  • Vue 用 Promise.then() 來安排「晚一點」的更新時機
  • nextTick 可以讓我們等到畫面更新完再繼續
  • 常見用途:focus 輸入框、捲動到底部、量元素尺寸

明天會繼續往下走,我們下一篇見。



上一篇
Vue 走過路過不要錯過 Day07 - watch 與 watchEffect:什麼時候該用哪一個
下一篇
Vue 走過路過不要錯過 Day09 - 從 {{ }} 到 v-bind:動態綁定 class 與 style
系列文
重新認識Vue 走過路過不要錯過9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言